feat(frontend): live countdown timer + search bar for bounties#887
feat(frontend): live countdown timer + search bar for bounties#887TeapoyY wants to merge 1 commit intoSolFoundry:mainfrom
Conversation
📝 WalkthroughWalkthroughThis PR introduces a new Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/components/bounty/BountyCountdown.tsx`:
- Around line 32-39: The expired branch in BountyCountdown returns early and
bypasses the variant handling, so expired states always render plain inline
markup; update BountyCountdown to respect the variant prop when expired by
moving the expired check into the variant branch or by conditionalizing the
badge rendering (i.e., when expired && variant === "badge" render the same badge
structure and classes as the non-expired badge branch, including using className
and the Clock icon) so callers passing variant="badge" get the badge markup even
after expiration.
In `@frontend/src/components/bounty/BountyGrid.tsx`:
- Around line 82-101: The search input in BountyGrid.tsx (the input bound to
searchQuery / setSearchQuery) lacks an accessible name; add one by giving the
input an id and associating a visible or visually-hidden <label> (using htmlFor)
or by adding a clear aria-label/aria-labelledby attribute (e.g.,
aria-label="Search bounties by title, description, or skill") so screen readers
receive a stable name; ensure the chosen approach matches the existing clear
button behavior and keeps the placeholder unchanged.
- Around line 35-49: The client-side filter (filteredBounties built from
allBounties) only searches already-fetched pages, so searching misses matches on
later pages and the displayed count/Load More behavior is wrong; change to
perform server-side search by passing debouncedSearch into the useBounties hook
(update the hook to accept a query param and return filtered pages and server
totalCount), then replace the client-only filtering (filteredBounties) with the
server-returned list, keep the "Load More" control functional during searches
(so more filtered pages can be fetched), and use the server totalCount for the
result count display instead of counting allBounties. Ensure you update
references to allBounties, filteredBounties, debouncedSearch, useBounties, and
the Load More/result count UI to use the new server-driven data.
- Line 25: The UI and filtering are inconsistent because filtering uses
debouncedSearch while empty states, result count, and pagination toggle still
read raw searchQuery; fix by deriving a single effective search string from the
debounced value (e.g., const effectiveSearch = debouncedSearch.trim()) and use
that everywhere the component decides "search mode" (filtering logic,
empty-state checks, result count display, and pagination toggle) so
whitespace-only input is treated as empty and the UI always reflects the dataset
actually rendered; update all places referencing searchQuery for these concerns
to use effectiveSearch instead.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: bc151fc3-74b4-42b2-ae52-25f083cc1653
📒 Files selected for processing (6)
frontend/src/components/bounty/BountyCard.tsxfrontend/src/components/bounty/BountyCountdown.tsxfrontend/src/components/bounty/BountyDetail.tsxfrontend/src/components/bounty/BountyGrid.tsxfrontend/src/lib/animations.tsfrontend/src/lib/utils.ts
| if (expired) { | ||
| return ( | ||
| <span className={`inline-flex items-center gap-1 text-status-error font-medium ${className}`}> | ||
| <Clock className="w-3.5 h-3.5" /> | ||
| Expired | ||
| </span> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Expired deadlines ignore the component’s badge variant.
Line 32 returns before the variant-specific branch on Lines 47-55 runs. Once a deadline expires, any caller that passes variant="badge" falls back to plain inline markup, so the new component only honors its public variant contract for non-expired states.
As per coding guidelines, frontend/**: React/TypeScript frontend. Check: Component structure and state management; Integration with existing components.
Also applies to: 47-55
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/bounty/BountyCountdown.tsx` around lines 32 - 39, The
expired branch in BountyCountdown returns early and bypasses the variant
handling, so expired states always render plain inline markup; update
BountyCountdown to respect the variant prop when expired by moving the expired
check into the variant branch or by conditionalizing the badge rendering (i.e.,
when expired && variant === "badge" render the same badge structure and classes
as the non-expired badge branch, including using className and the Clock icon)
so callers passing variant="badge" get the badge markup even after expiration.
| const allBounties = data?.pages.flatMap((p) => p.items) ?? []; | ||
|
|
||
| // Client-side search filter across title, description, and skills | ||
| const filteredBounties = useMemo(() => { | ||
| if (!debouncedSearch.trim()) return allBounties; | ||
| const q = debouncedSearch.toLowerCase(); | ||
| return allBounties.filter( | ||
| (b) => | ||
| b.title.toLowerCase().includes(q) || | ||
| b.description.toLowerCase().includes(q) || | ||
| b.skills?.some((s) => s.toLowerCase().includes(q)) || | ||
| (b.category ?? '').toLowerCase().includes(q) || | ||
| `${b.org_name ?? ''}/${b.repo_name ?? ''}`.toLowerCase().includes(q) | ||
| ); | ||
| }, [allBounties, debouncedSearch]); |
There was a problem hiding this comment.
Search only covers the pages that have already been fetched.
Line 35 builds allBounties from the pages currently in memory, and frontend/src/hooks/useBounties.ts:13-26 shows that hook paginates in batches of 12. Lines 39-48 therefore search only a partial dataset, while Line 191 hides Load More as soon as a query exists. That means matches on later pages become invisible, and the result count on Lines 169-172 is only a count over the loaded subset, not over all matching bounties.
As per coding guidelines, frontend/**: React/TypeScript frontend. Check: Component structure and state management; Integration with existing components; Error/loading/empty state handling.
Also applies to: 166-172, 191-191
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/bounty/BountyGrid.tsx` around lines 35 - 49, The
client-side filter (filteredBounties built from allBounties) only searches
already-fetched pages, so searching misses matches on later pages and the
displayed count/Load More behavior is wrong; change to perform server-side
search by passing debouncedSearch into the useBounties hook (update the hook to
accept a query param and return filtered pages and server totalCount), then
replace the client-only filtering (filteredBounties) with the server-returned
list, keep the "Load More" control functional during searches (so more filtered
pages can be fetched), and use the server totalCount for the result count
display instead of counting allBounties. Ensure you update references to
allBounties, filteredBounties, debouncedSearch, useBounties, and the Load
More/result count UI to use the new server-driven data.
| {/* Search bar */} | ||
| <div className="relative mb-6"> | ||
| <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted pointer-events-none" /> | ||
| <input | ||
| type="text" | ||
| value={searchQuery} | ||
| onChange={(e) => setSearchQuery(e.target.value)} | ||
| placeholder="Search bounties by title, description, or skill..." | ||
| className="w-full bg-forge-800 border border-border rounded-lg pl-10 pr-10 py-2.5 text-sm text-text-primary placeholder:text-text-muted focus:border-emerald outline-none transition-colors duration-150" | ||
| /> | ||
| {searchQuery && ( | ||
| <button | ||
| onClick={() => setSearchQuery('')} | ||
| className="absolute right-3 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-primary transition-colors" | ||
| aria-label="Clear search" | ||
| > | ||
| <X className="w-4 h-4" /> | ||
| </button> | ||
| )} | ||
| </div> |
There was a problem hiding this comment.
The new search field has no accessible name.
Lines 85-90 render the input without a <label>, aria-label, or aria-labelledby. Placeholder text is not a stable accessible name, so screen reader users will not get the purpose of the primary new control.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/bounty/BountyGrid.tsx` around lines 82 - 101, The
search input in BountyGrid.tsx (the input bound to searchQuery / setSearchQuery)
lacks an accessible name; add one by giving the input an id and associating a
visible or visually-hidden <label> (using htmlFor) or by adding a clear
aria-label/aria-labelledby attribute (e.g., aria-label="Search bounties by
title, description, or skill") so screen readers receive a stable name; ensure
the chosen approach matches the existing clear button behavior and keeps the
placeholder unchanged.
Implements multiple T1 bounties from SolFoundry/solfoundry: ### Bounty T1: Bounty Countdown Timer (SolFoundry#826) - New component: src/components/bounty/BountyCountdown.tsx - Real-time ticking countdown showing days, hours, minutes, seconds - Color changes to warning (<24h) and urgent (<1h) - Shows 'Expired' when deadline passes - Uses useEffect + setInterval for live updates (1s interval) - Integrated into BountyCard (replacing static timeLeft call) and BountyDetail sidebar ### Bounty T1: Add Search Bar to Bounties Page (SolFoundry#823) - Added debounced (300ms) search input to BountyGrid - Filters client-side across: title, description, skills, category, org/repo name - Clear button (X) to reset search - Result count shown above grid when searching - Empty state with link to clear search - Works alongside existing status and language filters ### Bounty T1: Mobile Responsive Polish (SolFoundry#824) - Added global overflow-x: hidden to html/body to prevent horizontal scroll on all pages - Fixed Footer contract address container with min-w-0 and flex-1 truncate to prevent overflow - Reduced terminal card font size on mobile (text-xs sm:text-sm) - Made stats strip wrap properly on mobile (gap-3 sm:gap-6, hidden sm:inline for separators) - All pages remain functional at 375px and 768px breakpoints ### Also included (was blocking build) - src/lib/utils.ts - created missing utilities: timeLeft, formatCurrency, LANG_COLORS, timeAgo, getTimeParts - src/lib/animations.ts - created missing framer-motion variants: fadeIn, cardHover, pageTransition, staggerContainer, staggerItem, buttonHover, slideInRight ### Acceptance Criteria - [x] Timer displays on bounty cards and detail page - [x] Updates without page refresh (real-time, every second) - [x] Visual urgency indicators (warning <24h, urgent <1h, expired) - [x] Search bar visible on /bounties page - [x] Typing filters bounties in real-time (debounced 300ms) - [x] Works alongside existing filters - [x] All pages look correct at 375px width - [x] All pages look correct at 768px width - [x] No horizontal scroll on any page
Summary
Implements multiple T1 bounties from SolFoundry/solfoundry:
Bounty T1: Bounty Countdown Timer (#826)
Bounty T1: Add Search Bar to Bounties Page (#823)
Bounty T1: Mobile Responsive Polish (#824)
Also included (was blocking build)
Acceptance Criteria